using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.Security.AccessControl;

namespace Fody
{
    public class WeavingTask : AppDomainIsolatedTask
    {
        private const string cacheBinPath = ".fody_cache";

        static object locker;

        static AppDomain appDomainChecking;

        static WeavingTask()
        {
#if DEBUG
            Debugger.Launch();
#endif

            locker = new object();

            appDomainChecking = AppDomain.CreateDomain("Fody_Checking", null,
                new AppDomainSetup
                {
                    ApplicationBase = AssemblyLocation.GetExecutionDirectory(),
                    PrivateBinPath = cacheBinPath,
                });
        }

        [Required]
        public string AssemblyPath { get; set; }

        public string DependenciesPath { get; set; }

        public string MessageImportance { set; get; }

        [Required]
        public string References { get; set; }

        public string KeyFilePath { get; set; }

        [Required]
        public string SolutionDir { get; set; }

        [Required]
        public string ProjectPath { get; set; }

        public string AddinSearchPaths { get; set; }

        BuildLogger logger;

        AppDomain appDomainExecution;

        public WeavingTask()
        {
            MessageImportance = "Low";
        }

        [LoaderOptimization(LoaderOptimization.MultiDomain)]
        public override bool Execute()
        {
            BuildEngine.LogMessageEvent(new BuildMessageEventArgs(string.Format("Fody v{0} executing...", GetType().Assembly.GetName().Version),
                "", "Fody", Microsoft.Build.Framework.MessageImportance.High));

            var stopwatch = Stopwatch.StartNew();

            logger = new BuildLogger
                         {
                             BuildEngine = BuildEngine,
                         };
            logger.Initialise(MessageImportance);

            try
            {
                AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;

                DependenciesPath = ResolveDependenciesPath();
                UpdateLocalBinAssemblies();
                LoadDependencies();
                
                appDomainChecking.DoCallBack(Inner);

                return true;
            }
            catch (Exception exception)
            {
                HandleException(exception);

                return false;
            }
            finally
            {
                stopwatch.Stop();
                logger.Flush();
                BuildEngine.LogMessageEvent(new BuildMessageEventArgs(string.Format("\tFinished Fody {0}ms.", stopwatch.ElapsedMilliseconds),
                    "", "Fody", Microsoft.Build.Framework.MessageImportance.High));
            }
        }

        Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            var foundAssembly = AppDomain.CurrentDomain.GetAssemblies().SingleOrDefault(a => a.FullName == args.Name);
            return foundAssembly;
        }

        void LoadDependencies()
        {
            var dependencyLoader = new DependencyLoader()
            {
                Logger = logger,
            };
            dependencyLoader.LoadDependencies(DependenciesPath);
        }

        string ResolveDependenciesPath()
        {
            var resolvedPaths = IoUtilities.ResolvePath(DependenciesPath, AssemblyLocation.GetExecutionDirectory()).ToArray();
            foreach (var path in resolvedPaths)
            {
                logger.LogInfo(string.Format("Resolved dependencies path to '{0}'.", path));
            }
            if (resolvedPaths.Length == 0)
            {
                throw new WeavingException("Dependencies path does not resolve to any known directory.");
            }
            else if (resolvedPaths.Length > 1)
            {
                throw new WeavingException("Dependencies path resolves to more than one known directory.");
            }
            return Path.GetFullPath(resolvedPaths[0]);
        }

        void HandleException(Exception exception)
        {
            if (exception is WeavingException)
            {
                logger.LogError(exception.Message);
            }
            else
            {
                logger.LogError(string.Format("Fatal error occurred. {0}", exception));
            }
        }

        void Inner()
        {
            var projectPathFinder = new ProjectPathFinder
                                        {
                                            Logger = logger,
                                            WeavingTask = this
                                        };
            projectPathFinder.Execute();

            var assemblyPathValidator = new AssemblyPathValidator
                                            {
                                                Logger = logger,
                                                WeavingTask = this
                                            };
            assemblyPathValidator.Execute();

            var projectWeaversFinder = new ProjectWeaversFinder
                                            {
                                                ProjectPathFinder = projectPathFinder,
                                                Logger = logger
                                            };
            projectWeaversFinder.Execute();

            var solutionPathValidator = new SolutionPathValidator
                                        {
                                            Logger = logger,
                                            WeavingTask = this
                                        };
            solutionPathValidator.Execute();

            var projectWeaversReader = new ProjectWeaversReader
                                           {
                                               Logger = logger,
                                               ProjectWeaversFinder = projectWeaversFinder
                                           };
            projectWeaversReader.Execute();

            FindWeavers(projectWeaversReader);

            var weaversXmlHistory = new WeaversXmlHistory
                                            {
                                                Logger = logger,
                                                ProjectWeaversFinder = projectWeaversFinder,
                                                WeavingTask = this
                                            };
            var fileChangedChecker = new FileChangedChecker
                                            {
                                                Logger = logger,
                                                AssemblyPath = AssemblyPath
                                            };

            if (!fileChangedChecker.ShouldStart())
            {
                weaversXmlHistory.CheckForChanged();
                return;
            }

            if (projectWeaversReader.Weavers.Count == 0)
            {
                logger.LogWarning(string.Format("Could not find any weavers. Either add weaver items to '{0}' or add a project named 'Weavers' to the solution.", ProjectWeaversFinder.ConfigFileName));
                return;
            }
            lock (locker)
            {
                ExecuteInOwnAppDomain(projectWeaversReader);
            }
            weaversXmlHistory.Flush();
        }

        void FindWeavers(ProjectWeaversReader projectWeaversReader)
        {
            var weaverProjectFileFinder = new WeaverProjectFileFinder
                                            {
                                                logger = logger,
                                                weavingTask = this
                                            };
            weaverProjectFileFinder.Execute();

            var addinFilesEnumerator = new AddinFilesEnumerator
                                            {
                                                AddinDirectories = FindAddinDirectories()
                                            };

            var weaverAssemblyPathFinder = new WeaverAssemblyPathFinder
                                            {
                                                AddinFilesEnumerator = addinFilesEnumerator,
                                                WeaverProjectFileFinder = weaverProjectFileFinder
                                            };

            var weaversConfiguredInstanceLinker = new WeaversConfiguredInstanceLinker
                                                        {
                                                            Logger = logger,
                                                            WeaverProjectFileFinder = weaverProjectFileFinder,
                                                            WeaverAssemblyPathFinder = weaverAssemblyPathFinder,
                                                            ProjectWeaversReader = projectWeaversReader
                                                        };
            weaversConfiguredInstanceLinker.Execute();

            var noWeaversConfiguredInstanceLinker = new NoWeaversConfiguredInstanceLinker
                                                        {
                                                            Logger = logger,
                                                            WeaverProjectFileFinder = weaverProjectFileFinder,
                                                            WeaverAssemblyPathFinder = weaverAssemblyPathFinder,
                                                            ProjectWeaversReader = projectWeaversReader
                                                        };
            noWeaversConfiguredInstanceLinker.Execute();
        }

        AddinDirectories FindAddinDirectories()
        {
            var nugetPackagePathFinder = new NugetPackagePathFinder
                                                {
                                                    Logger = logger,
                                                    WeavingTask = this
                                                };
            nugetPackagePathFinder.Execute();

            var addinDirectories = new AddinDirectories
                                        {
                                            Logger = logger
                                        };
            addinDirectories.Execute();

            var nugetDirectoryFinder = new NugetDirectoryFinder
                                            {
                                                Logger = logger,
                                                AddinDirectories = addinDirectories,
                                                NugetPackagePathFinder = nugetPackagePathFinder
                                            };
            nugetDirectoryFinder.Execute();

            var configDirectoryFinder = new ConfigDirectoryFinder
                                            {
                                                AddinDirectories = addinDirectories,
                                                Logger = logger,
                                                WeavingTask = this
                                            };
            configDirectoryFinder.Execute();

            var toolsDirectoryFinder = new ToolsDirectoryFinder
                                            {
                                                AddinDirectories = addinDirectories,
                                                Logger = logger,
                                                WeavingTask = this
                                            };
            toolsDirectoryFinder.Execute();

            return addinDirectories;
        }

        void ExecuteInOwnAppDomain(ProjectWeaversReader projectWeaversReader)
        {
            if (appDomainExecution == null || WeaversHistory.HasChanged(projectWeaversReader.Weavers.Select(x => x.AssemblyPath)))
            {
                if (appDomainExecution != null)
                {
                    AppDomain.Unload(appDomainExecution);
                }

                appDomainExecution = AppDomain.CreateDomain("Fody_Execution", null,
                    new AppDomainSetup
                        {
                            ApplicationBase = AssemblyLocation.GetExecutionDirectory(),
                            PrivateBinPath = cacheBinPath,
                        });
            }

            var innerWeavingTask = (InnerWeaver)appDomainExecution.CreateInstanceAndUnwrap(
                typeof(InnerWeaver).Assembly.FullName, typeof(InnerWeaver).FullName);
            innerWeavingTask.AssemblyPath = AssemblyPath;
            innerWeavingTask.References = References;
            innerWeavingTask.KeyFilePath = KeyFilePath;
            innerWeavingTask.Logger = logger;
            innerWeavingTask.Weavers = projectWeaversReader.Weavers;

            innerWeavingTask.Execute();
        }

        void UpdateLocalBinAssemblies()
        {
            var dependenciesDir = new DirectoryInfo(DependenciesPath);
            var localBinDir = new DirectoryInfo(Path.Combine(AssemblyLocation.GetExecutionDirectory(), cacheBinPath));
            if (!localBinDir.Exists)
            {
                localBinDir.Create();
                localBinDir.Refresh();
                localBinDir.Attributes |= FileAttributes.Hidden;
            }

            foreach (var sourceFile in dependenciesDir.EnumerateFiles())
            {
                var localBinFile = new FileInfo(Path.Combine(localBinDir.FullName, sourceFile.Name));
                if (!localBinFile.Exists || localBinFile.LastWriteTimeUtc < sourceFile.LastWriteTime)
                {
                    sourceFile.CopyTo(localBinFile.FullName, true);
                }
            }
        }
    }
}